package nakedb;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import prefuse.Constants;
import prefuse.Display;
import prefuse.Visualization;
import prefuse.action.ActionList;
import prefuse.action.RepaintAction;
import prefuse.action.distortion.BifocalDistortion;
import prefuse.action.distortion.Distortion;
import prefuse.action.distortion.FisheyeDistortion;
import prefuse.action.filter.GraphDistanceFilter;
import prefuse.action.layout.CircleLayout;
import prefuse.action.layout.graph.ForceDirectedLayout;
import prefuse.action.layout.graph.NodeLinkTreeLayout;
import prefuse.action.layout.graph.RadialTreeLayout;
import prefuse.controls.AnchorUpdateControl;
import prefuse.controls.ControlAdapter;
import prefuse.controls.DragControl;
import prefuse.controls.FocusControl;
import prefuse.controls.NeighborHighlightControl;
import prefuse.controls.PanControl;
import prefuse.controls.WheelZoomControl;
import prefuse.controls.ZoomControl;
import prefuse.controls.ZoomToFitControl;
import prefuse.data.Graph;
import prefuse.data.Tuple;
import prefuse.data.event.TupleSetListener;
import prefuse.data.io.DataIOException;
import prefuse.data.io.GraphMLReader;
import prefuse.data.tuple.TupleSet;
import prefuse.render.DefaultRendererFactory;
import prefuse.render.EdgeRenderer;
import prefuse.render.PolygonRenderer;
import prefuse.render.Renderer;
import prefuse.util.FontLib;
import prefuse.util.force.ForceSimulator;
import prefuse.util.ui.JFastLabel;
import prefuse.util.ui.JForcePanel;
import prefuse.util.ui.JValueSlider;
import prefuse.util.ui.UILib;
import prefuse.visual.AggregateItem;
import prefuse.visual.AggregateTable;
import prefuse.visual.VisualGraph;
import prefuse.visual.VisualItem;

/**
 * NakeDB
 * 
 * GaTech Spring 2008
 */
public class NakeDB extends JPanel implements ActionListener {

    private static final String graph = "graph";
    private static final String nodes = "graph.nodes";
    private static final String edges = "graph.edges";
    public static final String AGGR = "aggregates";
    static final String group = "group";
    static final String NODE_ATTRIBUTES = "attributes";
    
    
    public static final String NODE_DECORATORS = "nodeDeco";
    public static final String NODES = "graph.nodes";
    private LabelShapeRenderer labelShapeRenderer;
    
    private static Set<AggregateItem> aggregateItems;
    private Visualization m_vis;
    private static final String DATAFILE = "nakedb/Business.xml"; //NakeDB_XML02.xml
    // private static final String DATAFILE = "nakedb/Large500.xml"; 
    
    // Build from LayoutName
    private final String[] layoutLabels = {"FORCE_DIRECTED", "CIRCULAR", "RADIAL_TREE", "NODE_LINK"};
    private JList layoutList;
    
    private ActionList fishDistort;
    private ActionList bifoDistort;
   
    private LayoutInfo currentLayout;
    
    private Sidebar sidebar;
    
    private EdgeRenderer m_edgeRenderer;
    
    public enum LayoutName {FORCE_DIRECTED, CIRCULAR, RADIAL_TREE, NODE_LINK};
    public enum DistortionName {NO_DISTORTION, FISH_DISTORTION, BIFO_DISTORTION, FREE};
    
    private final Map<LayoutName, LayoutInfo>layouts;
    
    private JTabbedPane tabbedPane;
    
    public NakeDB(Graph g, String label) {
    	super(new BorderLayout());
    	
        // create a new, empty visualization for our data
        m_vis = new Visualization();
   
        // register the data with a visualization
        
        g.addColumn(VisualItem.LABEL, "CONCAT(name, ' ', '(', lines, ')')");
        g.addColumn("attributs", "CONCAT(name, '\\n', "
            + "REPLACE(attributes, '-', '\\n'))");        
        
        // adds graph to visualization and sets renderer label field
        setGraph(g, label);
        
        // fix selected focus nodes
        TupleSet focusGroup = m_vis.getGroup(Visualization.FOCUS_ITEMS); 
        focusGroup.addTupleSetListener(new TupleSetListener() {
            public void tupleSetChanged(TupleSet ts, Tuple[] add, Tuple[] rem)
            {
                for ( int i=0; i<rem.length; ++i )
                    ((VisualItem)rem[i]).setFixed(false);
                for ( int i=0; i<add.length; ++i ) {
                    ((VisualItem)add[i]).setFixed(false);
                    ((VisualItem)add[i]).setFixed(true);
                }
                if ( ts.getTupleCount() == 0 ) {
                    ts.addTuple(rem[0]);
                    ((VisualItem)rem[0]).setFixed(false);
                }
                m_vis.run("draw");
            }
        });
        
        
        
        // --------------------------------------------------------------------
        // create actions to process the visual data

        ActionList draw = new ActionList();
        
        // how many children connected to show
        int hops = 30;
        final GraphDistanceFilter filter = new GraphDistanceFilter(graph, hops);
        
        // Create layouts
        layouts = new HashMap<LayoutName, LayoutInfo>();
                       
        LayoutInfo layoutInfo = new LayoutInfo(LayoutName.FORCE_DIRECTED,
            new ForceDirectedLayout(graph));      
        layouts.put(layoutInfo.getName(), layoutInfo);
        currentLayout = layoutInfo; // default layout
        
        layoutInfo = new LayoutInfo(LayoutName.CIRCULAR,
            new CircleLayout(graph));      
        layouts.put(layoutInfo.getName(), layoutInfo);     

        layoutInfo = new LayoutInfo(LayoutName.RADIAL_TREE,
            new RadialTreeLayout(graph));      
        layouts.put(layoutInfo.getName(), layoutInfo);    
        
        layoutInfo = new LayoutInfo(LayoutName.NODE_LINK,
            new NodeLinkTreeLayout(graph));      
        layouts.put(layoutInfo.getName(), layoutInfo);        

        // fisheye distortion based on the current anchor location
        fishDistort = new ActionList();
        Distortion feye = new FisheyeDistortion(2, 2);
        fishDistort.add(feye);
        fishDistort.add(new RepaintAction());
        
        // bifocal distortion based on the current anchor location
        bifoDistort = new ActionList();
        Distortion bifo = new BifocalDistortion();
        bifoDistort.add(bifo);
        bifoDistort.add(new RepaintAction());        
       

        // finally, we register our ActionList with the Visualization.
        // we can later execute our Actions by invoking a method on our
        // Visualization, using the name we've chosen below.
        m_vis.putAction("draw", draw);
        
        m_vis.putAction(LayoutName.FORCE_DIRECTED.toString(), 
            layouts.get(LayoutName.FORCE_DIRECTED).getActionList());
        m_vis.putAction(LayoutName.CIRCULAR.toString(), 
            layouts.get(LayoutName.CIRCULAR).getActionList());
        m_vis.putAction(LayoutName.RADIAL_TREE.toString(), 
            layouts.get(LayoutName.RADIAL_TREE).getActionList());  
        m_vis.putAction(LayoutName.NODE_LINK.toString(), 
            layouts.get(LayoutName.NODE_LINK).getActionList());         

        m_vis.runAfter("draw", LayoutName.FORCE_DIRECTED.toString());
        m_vis.run(currentLayout.getName().toString());
        
        
        // --------------------------------------------------------------------
        // set up a display to show the visualization
        
        Display display = new Display(m_vis);
        // update the distortion anchor position to be the current
        // location of the mouse pointer
        display.addControlListener(new AnchorUpdateControl(feye, 
            DistortionName.FISH_DISTORTION.toString()));
        display.addControlListener(new AnchorUpdateControl(bifo, 
            DistortionName.BIFO_DISTORTION.toString()));
        display.setSize(700,750);
        
        //set the initial location of the visualization in the graph
        display.pan(200, 200);
        
        // main display controls
        display.addControlListener(new FocusControl(1));
        display.addControlListener(new DragControl());
        display.addControlListener(new PanControl());
        display.addControlListener(new ZoomControl());
        display.addControlListener(new WheelZoomControl());
        display.addControlListener(new ZoomToFitControl());
        display.addControlListener(new NeighborHighlightControl());
        
        display.setBackground(Color.WHITE);
        
        // --------------------------------------------------------------------        
        
        // create a panel for editing force values
        ForceSimulator fsim = ((ForceDirectedLayout)layouts.
            get(LayoutName.FORCE_DIRECTED).getActionList().get(0)).
            getForceSimulator();
        JForcePanel fpanel = new JForcePanel(fsim);
        fpanel.add(Box.createVerticalGlue());
        
        JPanel weightPanel = new JPanel();
        weightPanel.setBackground(Color.white);
        weightPanel.setLayout(new FlowLayout());
        weightPanel.setSize(310, 80);
        weightPanel.setLocation(0, 230);
        weightPanel.setBorder(BorderFactory.createTitledBorder("Label Rounded Corner"));
        
        final JValueSlider wslider = new JValueSlider("     Scaling", 1, 30, 8);
        wslider.addChangeListener(new ChangeListener() {
          public void stateChanged(ChangeEvent e) {
            labelShapeRenderer.setRoundedCorner(wslider.getValue().intValue(), 
                wslider.getValue().intValue());
            m_vis.run("draw");
          }
        });
       
        wslider.setValue(9);
        wslider.setBackground(Color.WHITE);
        wslider.setPreferredSize(new Dimension(280, 30));
        wslider.setMaximumSize(new Dimension(280, 30));
         
        Box df = new Box(BoxLayout.Y_AXIS);
        df.add(wslider);
        weightPanel.add(df);        
        fpanel.add(weightPanel);
        
        
        // Label and action on node over 
        final JFastLabel title = new JFastLabel("");
        title.setPreferredSize(new Dimension(200, 20));
        title.setHorizontalAlignment(SwingConstants.LEFT);
        title.setFont(FontLib.getFont("Tahoma", Font.PLAIN, 16));

        display.addControlListener(new ControlAdapter() {
          public void itemEntered(VisualItem item, MouseEvent e) {
            if (item.canGetString("name"))
              title.setText("           "  + item.getString("name"));
              m_vis.run("draw");
          }

          public void itemExited(VisualItem item, MouseEvent e) {
            title.setText(null);
            m_vis.run("draw");
          }
        });
        
        
        // SideBar
        sidebar = new Sidebar(this, m_vis, display, labelShapeRenderer, m_edgeRenderer, label, 
            graph, nodes, edges, filter, hops, title, aggregateItems);               
        
        //tabbed browsing
        tabbedPane = new JTabbedPane();
        ImageIcon icon = new ImageIcon("./nakedb/naked.png");
        
        tabbedPane.addTab("Basic", icon, sidebar, "Does nothing");
        tabbedPane.setMnemonicAt(0, KeyEvent.VK_1);
        
        tabbedPane.addTab("Advanced", icon, fpanel, "Does twice as much nothing");
        tabbedPane.setMnemonicAt(1, KeyEvent.VK_2);
        
        tabbedPane.addTab("Statistics", icon, new StatisticsPanel(this), "Does twice as much nothing");
        tabbedPane.setMnemonicAt(2, KeyEvent.VK_3);  
        
        // *********************************************************************
        
        // create a new JSplitPane to present the interface
        JSplitPane split = new JSplitPane();
        split.setLeftComponent(display);
        split.setRightComponent(tabbedPane);
        split.setOneTouchExpandable(true);
        split.setContinuousLayout(false);
        split.setDividerLocation(700);        
        
        // now we run our action list
        m_vis.run("draw");
        
        add(split);   
    }
    
    public void setGraph(Graph g, String label) {
      
      // add visual data groups
      VisualGraph vg2 = m_vis.addGraph(graph, g);
      m_vis.setInteractive(edges, null, false);
      m_vis.setValue(NODES, null, VisualItem.SHAPE,
              new Integer(Constants.SHAPE_ELLIPSE));
      
      AggregateTable at = m_vis.addAggregates(AGGR);
      at.addColumn(VisualItem.POLYGON, float[].class);
      at.addColumn("id", int.class);
      
      // add nodes to aggregates
      // create an aggregate for each 3-clique of nodes
      aggregateItems = new HashSet<AggregateItem>();
      Iterator nodes = vg2.nodes();
      int nb = vg2.getNodeCount();
      for ( int i=0; i<1; ++i ) {
          AggregateItem aitem = (AggregateItem)at.addItem();
          aggregateItems.add(aitem);
          aitem.setInt("id", i);
          for ( int j=0; nodes.hasNext() && j<nb/3; ++j ) {
              aitem.addItem((VisualItem)nodes.next());
          }         
          aitem = (AggregateItem)at.addItem();          
          aggregateItems.add(aitem);
          aitem.setInt("id", 2);
          for ( int j=0; nodes.hasNext(); ++j ) {
              aitem.addItem((VisualItem)nodes.next());
          }
      } 
      
        // update labeling
        labelShapeRenderer = new LabelShapeRenderer(label, 30);
        
        DefaultRendererFactory drf = new DefaultRendererFactory(labelShapeRenderer);
        Renderer polyR = new PolygonRenderer(Constants.POLY_TYPE_CURVE);
        ((PolygonRenderer)polyR).setCurveSlack(0.15f);
        drf.add("ingroup('aggregates')", polyR);
        
        m_vis.setRendererFactory(drf);
        m_edgeRenderer = new EdgeRenderer();
        m_edgeRenderer.setArrowType(2);
        m_edgeRenderer.setArrowHeadSize(0, 0);
        drf.setDefaultEdgeRenderer(m_edgeRenderer);        

        VisualItem f = (VisualItem)vg2.getNode(0);
        m_vis.getGroup(Visualization.FOCUS_ITEMS).setTuple(f);
        f.setFixed(false);
    }
    
    public void actionPerformed(ActionEvent e) {
      
      // Code HORRIBLE and a Map should be use and optimizations!!
      // Don't check if JComboBox all the time !!
      //  if(((JComboBox)e.getSource())==layoutCombo){
        JComboBox cb = (JComboBox)e.getSource();
        if(((String)cb.getSelectedItem()).equals("Force Directed")){
          if (currentLayout.getName() != LayoutName.FORCE_DIRECTED) {
        	tabbedPane.setEnabledAt(1, true);
            m_vis.cancel(currentLayout.getName().toString());
            m_vis.run(LayoutName.FORCE_DIRECTED.toString());
            currentLayout = layouts.get(LayoutName.FORCE_DIRECTED);
            sidebar.resetDistortionBox();
          }          
        }else if(((String)cb.getSelectedItem()).equals("Radial Tree")){
          if (currentLayout.getName() != LayoutName.RADIAL_TREE) {
        	tabbedPane.setEnabledAt(1, false);
            m_vis.cancel(currentLayout.getName().toString());
            m_vis.run(LayoutName.RADIAL_TREE.toString());
            currentLayout = layouts.get(LayoutName.RADIAL_TREE);
            sidebar.resetDistortionBox();
          }          
        }else if(((String)cb.getSelectedItem()).equals("Circular")){
          if (currentLayout.getName() != LayoutName.CIRCULAR) {
        	tabbedPane.setEnabledAt(1, false);
            m_vis.cancel(currentLayout.getName().toString());
            m_vis.run(LayoutName.CIRCULAR.toString());
            currentLayout = layouts.get(LayoutName.CIRCULAR);   
            sidebar.resetDistortionBox();
          }         
        }else if(((String)cb.getSelectedItem()).equals("Node Link")){
          if (currentLayout.getName() != LayoutName.NODE_LINK) {
        	tabbedPane.setEnabledAt(1, false);
            m_vis.cancel(currentLayout.getName().toString());
            m_vis.run(LayoutName.NODE_LINK.toString());
            currentLayout = layouts.get(LayoutName.NODE_LINK);
            sidebar.resetDistortionBox();
          }    
        } else if ("Default".equals((String)cb.getSelectedItem())) {
          m_vis.removeAction(DistortionName.BIFO_DISTORTION.toString());
          m_vis.removeAction(DistortionName.FISH_DISTORTION.toString());
          currentLayout.setDurationInfinity();
          currentLayout.addRepaintAction();         
          sidebar.getDraw().add(sidebar.getDataSizeAction());
        } else if ("Free to move".equals((String)cb.getSelectedItem())) {
          m_vis.removeAction(DistortionName.BIFO_DISTORTION.toString());
          m_vis.removeAction(DistortionName.FISH_DISTORTION.toString());
          currentLayout.setDurationNonInfinity();
          currentLayout.addRepaintAction();     
          sidebar.getDraw().add(sidebar.getDataSizeAction());
        } else if ("Fisheye".equals((String)cb.getSelectedItem())) {
          currentLayout.removeRepaintAction();          
          m_vis.removeAction(DistortionName.BIFO_DISTORTION.toString());
          m_vis.putAction(DistortionName.FISH_DISTORTION.toString(), fishDistort);
          currentLayout.setDurationInfinity();        
          sidebar.getDraw().remove(sidebar.getDataSizeAction());
        } else if ("Bifocal".equals((String)cb.getSelectedItem())) {
          currentLayout.removeRepaintAction();
          m_vis.removeAction(DistortionName.FISH_DISTORTION.toString());
          m_vis.putAction(DistortionName.BIFO_DISTORTION.toString(), bifoDistort);
          currentLayout.setDurationInfinity();         
          sidebar.getDraw().remove(sidebar.getDataSizeAction());
        } else {
          System.out.println("Error unknown selection : " + (String)cb.getSelectedItem());
        }     
    }
    
    // Main and demo methods
    
    public static void main(String[] args) {
        UILib.setPlatformLookAndFeel();
        
        JFrame frame = demo(DATAFILE, "name");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        //Disable aggregate items by default
        for (AggregateItem item : aggregateItems) {
          item.setVisible(false);
        } 
    }
    
    public static void run() {
      UILib.setPlatformLookAndFeel();
      
      JFrame frame = demo(DATAFILE, "name");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      
      //Disable aggregate items by default
      for (AggregateItem item : aggregateItems) {
        item.setVisible(false);
      } 
  }    
    
    public static JFrame demo(String datafile, String label) {
        Graph g = null;
        try {
          g = new GraphMLReader().readGraph(datafile);
        } catch (DataIOException e) {
          e.printStackTrace();
        }
        
        return demo(g, label);
    }
    
    public static JFrame demo(Graph g, String label) {
        final NakeDB view = new NakeDB(g, label);

        // launch window
        JFrame frame = new JFrame("NakeDB : Database Visualization");
        frame.setContentPane(view);
        frame.pack();
        frame.setVisible(true);
        
        frame.addWindowListener(new WindowAdapter() {
            public void windowActivated(WindowEvent e) {
                view.m_vis.run("layout");
            }
            public void windowDeactivated(WindowEvent e) {
                view.m_vis.cancel("layout");
            }
        });
        
        return frame;
    }
    
    public int getTotalNumberOfTables(){
  	  	return m_vis.size(nodes);
    }
    public int getTotalNumberOfConnections(){
  	  	return m_vis.size(edges);
    }
    public int getGraphSize(){
  	  	return m_vis.size(graph);
    }
}
